package LDraw.Support; import Common.Box2; import Common.Box3; import Common.Matrix4; import Common.Size2; import Common.Vector2f; import Common.Vector3f; import Common.Vector4f; import LDraw.Support.type.LocationModeT; import LDraw.Support.type.ProjectionModeT; /* Who owns what data? Things appkit knows about: Scroll position owned by Appkit. Zoom owned by camera. Clip view scale factor owned by NS and slaved from zoom by camera _sometimes_. Document Size owned by GL view, controlled by camera Things OpenGL knows about: viewport - always set to visible area of GL drawable by view code - the camera assumes this is true. transform matrices - always owned by camera. */ /* Who owns what data? Things appkit knows about: Scroll position owned by Appkit. Zoom owned by camera. Clip view scale factor owned by NS and slaved from zoom by camera _sometimes_. Document Size owned by GL view, controlled by camera Things OpenGL knows about: viewport - always set to visible area of GL drawable by view code - the camera assumes this is true. transform matrices - always owned by camera. */ public class LDrawGLCamera { public static final int NO_ROUNDING_DOC_SIZE = 0; // controls perspective; cameraLocation = modelSize * CAMERA_DISTANCE_FACTOR public static final float CAMERA_DISTANCE_FACTOR = 6.5f; // Turn-table view changes how rotations work public static final boolean USE_TURNTABLE = false; public static final float WALKTHROUGH_NEAR = 20.0f; public static final float WALKTHROUGH_FAR = 20000.0f; protected ILDrawGLCameraScroller scroller; protected float[] projection = new float[16]; protected float[] modelView = new float[16]; protected float[] orientation = new float[16]; protected ProjectionModeT projectionMode; protected LocationModeT locationMode; Box3 modelSize; boolean viewportExpandsToAvailableSize; float zoomFactor; protected float cameraDistance; // location of camera on the z-axis; distance from // (0,0,0); protected Vector3f rotationCenter; Size2 snugFrameSize; int mute; // Counted 'mute' to stop re-entrant calls to tickle... // Normally the doc size is rounded so that it doesn't jump per frame as we // nudge; we can turn this OFF to debug editing. public LDrawGLCamera() { init(); } // ========== init // ============================================================== // // Purpose: Sets up the new camera. // // Notes: The camera isn't really useful until a scroller is attached and // the camera is then tickled. Without a scroller, the camera // cannot complete its setup. // // ============================================================================== public LDrawGLCamera init() { viewportExpandsToAvailableSize = true; zoomFactor = 100; // percent cameraDistance = -10000; projectionMode = ProjectionModeT.ProjectionModePerspective; locationMode = LocationModeT.LocationModeModel; modelSize = Box3.getInvalidBox(); rotationCenter = Vector3f.getZeroVector3f(); snugFrameSize = Size2.getZeroSize2(); GLMatrixMath.buildRotationMatrix(orientation, 180, 1, 0, 0); GLMatrixMath.buildIdentity(modelView); GLMatrixMath.buildIdentity(projection); return this; }// end init // ========== setScroller: // ====================================================== // // Purpose: Specifies a scroller protocol that the camera uses to get // information about the document. // // Notes: While the simplest design might be for the camer to control all // apsects of viewing, it can't own scrolling; AppKit needs to own // scrolling state and having the data exist in two places is a // recipe for chaos. // // So the scroller gives the camera an abstract way to ask // _someone_ what's going on in the NS world with scrolling without // having to have our app's NS structure coded into the camera. // // ============================================================================== public void setScroller(ILDrawGLCameraScroller newScroller) { scroller = newScroller; }// end setScroller: // #pragma mark - // #pragma mark PUBLIC ACCESSORS // #pragma mark - // ========== getProjection // ===================================================== // // Purpose: Returns the current projection matrix as a float[16] ptr. // The projection matrix handles the effects of scrolling and // zoom. // // Notes: The camera class does not talk to OpenGL directly, and thus // does not need context access. The current matrices are owned // by the camera. Rendering engine code is responsible for syncing // OpenGL to the camera, or shoveling these matrices into its // custom shaders. // // ============================================================================== public float[] getProjection() { return projection; }// end getProjection // ========== getModelView // ====================================================== // // Purpose: Returns the current modelview matrix as a float[16] ptr. // The modelview matrix accounts for camera view distance, model // rotation and model center changes. // // ============================================================================== public float[] getModelView() { return modelView; }// end getModelView // ========== zoomPercentage // ==================================================== // // Purpose: Returns the current zoom percentage. // // ============================================================================== public float zoomPercentage() { return zoomFactor; }// end zoomPercentage // ========== projectionMode // ==================================================== // // Purpose: Returns the current projection mode (perspective or ortho). // // ============================================================================== public ProjectionModeT projectionMode() { return projectionMode; }// end projectionMode // ========== locationMode // ==================================================== // // Purpose: Returns the current location mode. // // ============================================================================== public LocationModeT locationMode() { return locationMode; }// end locationMode // ========== viewingAngle // ====================================================== // // Purpose: Returns the current viewing angle as a triplet of Euler angles. // // ============================================================================== public Vector3f viewingAngle() { Matrix4 transformation = Matrix4.getIdentityMatrix4(); TransformComponents components = TransformComponents .getIdentityComponents(); Vector3f degrees = Vector3f.getZeroVector3f(); transformation = MatrixMath.Matrix4CreateFromGLMatrix4(getModelView()); transformation = MatrixMath.Matrix4Rotate(transformation, MatrixMath.V3Make(180, 0, 0)); // LDraw is upside-down MatrixMath.Matrix4DecomposeTransformation(transformation, components); degrees = components.rotate; degrees.setX((float) Math.toDegrees(degrees.getX())); degrees.setY((float) Math.toDegrees(degrees.getY())); degrees.setZ((float) Math.toDegrees(degrees.getZ())); return degrees; }// end viewingAngle public Vector3f rotationCenter() { return rotationCenter; } // #pragma mark - // #pragma mark INTERNAL UTILITIES // #pragma mark - // ========== scrollCenterToPoint: // ============================================== // // Purpose: Scrolls a given model point to the center of the visible window. // // Notes: This utility does not 'tickle' the camera - client code must do // this. // // ============================================================================== public void scrollCenterToPoint(Vector2f newCenter) { Box2 newVisibleRect = scroller.getVisibleRect(); Vector2f scrollOrigin = MatrixMath .V2Make(newCenter.getX() - MatrixMath.V2BoxWidth(scroller.getVisibleRect()) / 2, newCenter.getY() - MatrixMath.V2BoxHeight(scroller .getVisibleRect()) / 2); // Sanity check if (scrollOrigin.getX() < 0) { scrollOrigin.setX(0); } if (scrollOrigin.getY() < 0) { scrollOrigin.setY(0); } newVisibleRect.setOrigin(scrollOrigin); scroller.setScrollOrigin(newVisibleRect.getOrigin()); }// end scrollCenterToPoint: // ========== fieldDepth // ======================================================== // // Purpose: Returns the depth range of our view - that is, the distance // between the near an far clip planes in model coordinates. The // model origin is centered in this range. // // ============================================================================== public float fieldDepth() { float fieldDepth = 0; // This is effectively equivalent to infinite field depth fieldDepth = Math.max(snugFrameSize.getHeight(), snugFrameSize.getWidth()); fieldDepth *= 2; return fieldDepth; }// end fieldDepth // ========== nearOrthoClippingRectFromVisibleRect: // ============================ // // Purpose: Returns the rect of the near clipping plane which should be used // for an orthographic projection. The coordinates are in model // coordinates, located on the plane at // z = - fieldDepth() / 2. // // ============================================================================== public Box2 nearOrthoClippingRectFromVisibleRect(Box2 visibleRectIn) { Box2 visibilityPlane = Box2.getZeroBox2(); float y = MatrixMath.V2BoxMinY(visibleRectIn); if (true)// isFlipped] == true;) { y = scroller.getDocumentSize().getHeight() - y - MatrixMath.V2BoxHeight(visibleRectIn); } // The projection plane is stated in model coordinates. visibilityPlane.getOrigin().setX( MatrixMath.V2BoxMinX(visibleRectIn) - scroller.getDocumentSize().getWidth() / 2); visibilityPlane.getOrigin().setY( y - scroller.getDocumentSize().getHeight() / 2); visibilityPlane.getSize() .setWidth(MatrixMath.V2BoxWidth(visibleRectIn)); visibilityPlane.getSize().setHeight( MatrixMath.V2BoxHeight(visibleRectIn)); return visibilityPlane; }// end nearOrthoClippingRectFromVisibleRect: // ========== nearFrustumClippingRectFromVisibleRect: // ========================== // // Purpose: Returns the rect of the near clipping plane which should be used // for an perspective projection. The coordinates are in model // coordinates, located on the plane at // z = - fieldDepth() / 2. // // Notes: We want perspective and ortho views to show objects at the // origin as the same size. Since perspective viewing is defined // by a frustum (truncated pyramid), we have to shrink the // visibily plane--which is located on the near clipping plane--in // such a way that the slice of the frustum at the origin will // have the dimensions of the desired visibility plane. (Remember, // slices grow *bigger* as they go deeper into the view. Since the // origin is deeper, that means we need a near visibility plane // that is *smaller* than the desired size at the origin.) // // ============================================================================== public Box2 nearFrustumClippingRectFromVisibleRect(Box2 visibleRectIn) { Box2 orthoVisibilityPlane = nearOrthoClippingRectFromVisibleRect(visibleRectIn); Box2 visibilityPlane = orthoVisibilityPlane; float fieldDepth = fieldDepth(); // Find the scaling percentage betwen the frustum slice through // (0,0,0) and the slice that defines the near clipping plane. float visibleProportion = (Math.abs(cameraDistance) - fieldDepth / 2) / Math.abs(cameraDistance); // scale down the visibility plane, centering it in the full-size one. visibilityPlane.getOrigin().setX( MatrixMath.V2BoxMinX(orthoVisibilityPlane) + MatrixMath.V2BoxWidth(orthoVisibilityPlane) * (1 - visibleProportion) / 2); visibilityPlane.getOrigin().setY( MatrixMath.V2BoxMinY(orthoVisibilityPlane) + MatrixMath.V2BoxHeight(orthoVisibilityPlane) * (1 - visibleProportion) / 2); visibilityPlane.getSize() .setWidth( MatrixMath.V2BoxWidth(orthoVisibilityPlane) * visibleProportion); visibilityPlane.getSize().setHeight( MatrixMath.V2BoxHeight(orthoVisibilityPlane) * visibleProportion); return visibilityPlane; }// end nearFrustumClippingRectFromVisibleRect: // ========== nearOrthoClippingRectFromNearFrustumClippingRect: // ================= // // Purpose: Returns the near clipping rectangle which would be used if the // given perspective view were converted to an orthographic // projection. // // ============================================================================== public Box2 nearOrthoClippingRectFromNearFrustumClippingRect( Box2 visibilityPlane) { Box2 orthoVisibilityPlane = Box2.getZeroBox2(); float fieldDepth = fieldDepth(); // Find the scaling percentage betwen the frustum slice through // (0,0,0) and the slice that defines the near clipping plane. float visibleProportion = (Math.abs(cameraDistance) - fieldDepth / 2) / Math.abs(cameraDistance); // Enlarge the ortho plane orthoVisibilityPlane.getSize().setWidth( visibilityPlane.getSize().getWidth() / visibleProportion); orthoVisibilityPlane.getSize().setHeight( visibilityPlane.getSize().getHeight() / visibleProportion); // Move origin according to enlargement orthoVisibilityPlane.getOrigin().setX( MatrixMath.V2BoxMinX(visibilityPlane) - MatrixMath.V2BoxWidth(orthoVisibilityPlane) * (1 - visibleProportion) / 2); orthoVisibilityPlane.getOrigin().setY( MatrixMath.V2BoxMinY(visibilityPlane) - MatrixMath.V2BoxHeight(orthoVisibilityPlane) * (1 - visibleProportion) / 2); return orthoVisibilityPlane; }// end nearOrthoClippingRectFromNearFrustumClippingRect: // ========== visibleRectFromNearOrthoClippingRect: // ============================= // // Purpose: Returns the Cocoa view visible rectangle which would result in // the given orthographic clipping rect. // // ============================================================================== public Box2 visibleRectFromNearOrthoClippingRect(Box2 visibilityPlane) { Box2 newVisibleRect = Box2.getZeroBox2(); // Convert from model coordinates back to Cocoa view coordinates. newVisibleRect.getOrigin().setX( visibilityPlane.getOrigin().getX() + scroller.getDocumentSize().getWidth() / 2); newVisibleRect.getOrigin().setY( visibilityPlane.getOrigin().getY() + scroller.getDocumentSize().getHeight() / 2); newVisibleRect.setSize(visibilityPlane.getSize()); if (true)// isFlipped] == true;) { newVisibleRect.getOrigin().setY( scroller.getDocumentSize().getHeight() - MatrixMath.V2BoxHeight(visibilityPlane) - MatrixMath.V2BoxMinY(newVisibleRect)); } return newVisibleRect; }// end visibleRectFromNearOrthoClippingRect: // ========== visibleRectFromNearFrustumClippingRect: // =========================== // // Purpose: Returns the Cocoa view visible rectangle which would result in // the given frustum clipping rect. // // ============================================================================== public Box2 visibleRectFromNearFrustumClippingRect(Box2 visibilityPlane) { Box2 orthoClippingRect = Box2.getZeroBox2(); Box2 newVisibleRect = Box2.getZeroBox2(); orthoClippingRect = nearOrthoClippingRectFromNearFrustumClippingRect(visibilityPlane); newVisibleRect = visibleRectFromNearOrthoClippingRect(orthoClippingRect); return newVisibleRect; }// end visibleRectFromNearFrustumClippingRect: // ========== makeProjection // ==================================================== // // Purpose: Returns the Cocoa view visible rectangle which would result in // the given frustum clipping rect. // // ============================================================================== public void makeProjection() { float fieldDepth = fieldDepth(); Box2 visibilityPlane = Box2.getZeroBox2(); // ULTRA-IMPORTANT falseTE: this method assumes that you have already // made our // openGLContext the current context // Start from scratch if (locationMode == LocationModeT.LocationModeWalkthrough) { Size2 viewportSize = scroller.getMaxVisibleSizeDoc(); float aspect_ratio = viewportSize.getWidth() / viewportSize.getHeight(); GLMatrixMath.buildFrustumMatrix(projection, -WALKTHROUGH_NEAR / (zoomFactor / 100.0f), +WALKTHROUGH_NEAR / (zoomFactor / 100.0f), -WALKTHROUGH_NEAR / (zoomFactor / 100.0f) / aspect_ratio, +WALKTHROUGH_NEAR / (zoomFactor / 100.0f) / aspect_ratio, WALKTHROUGH_NEAR, WALKTHROUGH_FAR); } else if (projectionMode == ProjectionModeT.ProjectionModePerspective) { visibilityPlane = nearFrustumClippingRectFromVisibleRect(scroller .getVisibleRect()); assert (visibilityPlane.getSize().getWidth() > 0.0); assert (visibilityPlane.getSize().getHeight() > 0.0); GLMatrixMath.buildFrustumMatrix(projection, MatrixMath.V2BoxMinX(visibilityPlane), // left MatrixMath.V2BoxMaxX(visibilityPlane), // right MatrixMath.V2BoxMinY(visibilityPlane), // bottom MatrixMath.V2BoxMaxY(visibilityPlane), // top Math.abs(cameraDistance) - fieldDepth / 2, // near (closer // points are // clipped); // distance from // CAMERA // LOCATION Math.abs(cameraDistance) + fieldDepth / 2 // far (points // beyond this // are clipped); // distance from // CAMERA // LOCATION ); } else { visibilityPlane = nearOrthoClippingRectFromVisibleRect(scroller .getVisibleRect()); assert (visibilityPlane.getSize().getWidth() > 0.0); assert (visibilityPlane.getSize().getHeight() > 0.0); GLMatrixMath.buildOrthoMatrix(projection, MatrixMath.V2BoxMinX(visibilityPlane), // left MatrixMath.V2BoxMaxX(visibilityPlane), // right MatrixMath.V2BoxMinY(visibilityPlane), // bottom MatrixMath.V2BoxMaxY(visibilityPlane), // top Math.abs(cameraDistance) - fieldDepth / 2, // near (points // beyond these // are clipped) Math.abs(cameraDistance) + fieldDepth / 2); // far } }// end makeProjection // ========== makeModelView // ===================================================== // // Purpose: Rebuilds the model-view matrix from the camera distance, // rotation and center - call this if any of these change. // // ============================================================================== public void makeModelView() { float cam_trans[] = new float[16]; float[] center_trans = new float[16]; float[] flip = new float[16]; float[] temp1 = new float[16]; float[] temp2 = new float[16]; GLMatrixMath.buildRotationMatrix(flip, 0, 1, 0, 0); GLMatrixMath.buildTranslationMatrix(cam_trans, 0, 0, cameraDistance); GLMatrixMath.buildTranslationMatrix(center_trans, -rotationCenter.getX(), -rotationCenter.getY(), -rotationCenter.getZ()); if (locationMode == LocationModeT.LocationModeModel) { GLMatrixMath.buildIdentity(temp1); GLMatrixMath.multMatrices(temp2, temp1, cam_trans); GLMatrixMath.multMatrices(temp1, temp2, orientation); GLMatrixMath.multMatrices(temp2, temp1, center_trans); GLMatrixMath.multMatrices(modelView, temp2, flip); } else { GLMatrixMath.buildIdentity(temp1); GLMatrixMath.multMatrices(temp2, temp1, orientation); GLMatrixMath.multMatrices(temp1, temp2, center_trans); GLMatrixMath.multMatrices(modelView, temp1, flip); } }// end makeModelView // ========== tickle // ============================================================ // // Purpose: Cause the camera to recompute the document size, scrolling // position, and all matrices. // // Notes: This routine must be called any time the external scroller // properties change, so that the camera can 'react' to the change. // // ============================================================================== public void tickle() { if(mute!=0)return; // At init we get tickled before we are wired - avoid seg fault or NaNs. if (scroller != null) { // // First, recalculate the document size based on the current model // size, zoom, and current window size. // We will recalculate camera distance and rebuild the MV matrix. // / Vector3f origin = new Vector3f(new float[] { 0, 0, 0 }); Vector2f centerPoint = MatrixMath.V2Make( MatrixMath.V2BoxMidX(scroller.getVisibleRect()), MatrixMath.V2BoxMidY(scroller.getVisibleRect())); Box3 newBounds = modelSize; if (MatrixMath.V3EqualBoxes(newBounds, Box3.getInvalidBox()) == true || newBounds.getMin().getX() >= newBounds.getMax().getX() || newBounds.getMin().getY() >= newBounds.getMax().getY() || newBounds.getMin().getZ() >= newBounds.getMax().getZ()) { newBounds = MatrixMath.V3BoundsFromPoints( MatrixMath.V3Make(-1, -1, -1), MatrixMath.V3Make(1, 1, 1)); } // // Find bounds size, based on model dimensions. // float distance1 = MatrixMath.V3DistanceBetween2Points(origin, newBounds.getMin()); float distance2 = MatrixMath.V3DistanceBetween2Points(origin, newBounds.getMax()); float newSize = Math.max(distance1, distance2) + 40; // 40 is just // to // provide a // margin. // The canvas resizing is set to a fairly large granularity so // it doesn't constantly change on people. if (NO_ROUNDING_DOC_SIZE == 0) newSize = (float) (Math.ceil(newSize / 384) * 384); cameraDistance = -(newSize) * CAMERA_DISTANCE_FACTOR; makeModelView(); // New camera distance means rebuild Mv. // // Second, resize the document based on the model size and the // parent window size. // We will restore scrolling, which can get borked when the document // size changes. // Size2 oldFrameSize = scroller.getDocumentSize(); Size2 newFrameSize = Size2.getZeroSize2(); snugFrameSize = MatrixMath.V2MakeSize(newSize * 2, newSize * 2); if (viewportExpandsToAvailableSize == true) { // Make the frame either just a little bit bigger than the // size of the model, or the same as the scroll view, // whichever is larger. newFrameSize = MatrixMath.V2MakeSize( Math.max(snugFrameSize.getWidth(), scroller .getMaxVisibleSizeDoc().getWidth()), Math.max( snugFrameSize.getHeight(), scroller .getMaxVisibleSizeDoc().getHeight())); } else { newFrameSize = snugFrameSize; } newFrameSize.setWidth((float) Math.floor(newFrameSize.getWidth())); newFrameSize .setHeight((float) Math.floor(newFrameSize.getHeight())); // The canvas size changes will effectively be distributed equally // on all sides, because the model is always drawn in the center of // the canvas. So, our effective viewing center will only change by // half the size difference. centerPoint.setX(centerPoint.getX() + (newFrameSize.getWidth() - oldFrameSize.getWidth()) / 2); centerPoint .setY(centerPoint.getY() + (newFrameSize.getHeight() - oldFrameSize .getHeight()) / 2); if (locationMode == LocationModeT.LocationModeModel) { // I have only seen this on Lion and later: when we set the // document size the scroll point is set to something totally // silly. Because of this, the visible rect is empty, and the // entire camera calculation NaNs out. // To 'work around' this, we ignore the tickle that comes back // from the reshape that is a result of the doc frame size // changing; we don't need it since we're going to re-scroll and // redo the MV projection in the next few lines. mute++; scroller.setDocumentSize(newFrameSize); scrollCenterToPoint(centerPoint); // Restore centering - // changing the doc size // causes AppKit to whack // scrolling. mute--; } else { mute++; scroller.setDocumentSize(scroller.getMaxVisibleSizeDoc()); mute--; } // Rebuild projection based on latest scroll data from AppKit. makeProjection(); } }// end tickle // #pragma mark - // #pragma mark CAMERA CONTROL API // #pragma mark - // ========== setModelSize: // ===================================================== // // Purpose: Tell the camera the new size of the model it is viewing. // // Notes: The tickle command will recompute the document size and then // request a scrolling update. // // ============================================================================== public void setModelSize(Box3 inModelSize) { assert (inModelSize.getMin().getX() != inModelSize.getMax().getX() || inModelSize.getMin().getY() != inModelSize.getMax().getY() || inModelSize .getMin().getZ() != inModelSize.getMax().getZ()); modelSize = inModelSize; tickle(); }// end setModelSize: // ========== setRotationCenter: // ============================================= // // Purpose: Change the rotation center to a new location, and center that // location. // // ============================================================================== public void setRotationCenter(Vector3f point) { if (MatrixMath.V3EqualPoints(rotationCenter, point) == false) { rotationCenter = point; makeModelView(); // Recalc model view - needed before we can scroll // to a given point! scrollModelPoint(rotationCenter, MatrixMath.V2Make(0.5f, 0.5f)); // scroll // to // new // center // (tickles // itself, // public // API) } }// end setRotationCenter: // ========== setZoomPercentage: // ================================================ // // Purpose: Change the zoom of the camera. This is called by the zoom // text field and zoom commands. It resizes the document and // tickles the camera to make everything take effect. // // ============================================================================== public void setZoomPercentage(float newPercentage) { // assert(!isnan(newPercentage)); // assert(!isinf(newPercentage)); if (newPercentage < 1.0f) // Hard clamp against crazy-small zoom-out. newPercentage = 1.0f; float currentZoomPercentage = zoomFactor; // Don't zoom if the zoom level isn't actually changing (to avoid // unnecessary re-draw) if (currentZoomPercentage == newPercentage) return; Vector2f centerPoint = MatrixMath.V2Make( MatrixMath.V2BoxMidX(scroller.getVisibleRect()), MatrixMath.V2BoxMidY(scroller.getVisibleRect())); Vector2f centerFraction = MatrixMath.V2Make(centerPoint.getX() / scroller.getDocumentSize().getWidth(), centerPoint.getY() / scroller.getDocumentSize().getHeight()); zoomFactor = newPercentage; // Tell NS that sizes have changed - once we do this, we can request a // re-scroll. mute++; if (locationMode == LocationModeT.LocationModeWalkthrough) scroller.setScaleFactor(1.0f); else scroller.setScaleFactor(zoomFactor / 100.0f); centerPoint.setX(centerFraction.getX() * scroller.getDocumentSize().getWidth()); centerPoint.setY(centerFraction.getY() * scroller.getDocumentSize().getHeight()); if (locationMode != LocationModeT.LocationModeWalkthrough) scrollCenterToPoint(centerPoint); // Request that NS change // scrolling to restore // centering. mute--; tickle(); // Rebuild ourselves based on the new zoom, scroll, etc. }// end setZoomPercentage: // ========== setZoomPercentage:preservePoint: // ================================== // // Purpose: Set the zoom percentage, keeping a particular model point fixed // on screen. // // Notes: To do this, we figure out where on screen the model point is, // then we zoom, and then we re-scroll that 3-d point to its new // location. // // ============================================================================== public void setZoomPercentage(float newPercentage, Vector3f modelPoint) { Box2 viewport = MatrixMath.V2MakeBox(0, 0, 1, 1); // Fake view-port - // this gets us our // scaled point in // viewport-proportional // units. // - Near clipping plane unprojection Vector3f nearModelPoint = MatrixMath.V3Project(modelPoint, MatrixMath.Matrix4CreateFromGLMatrix4(modelView), MatrixMath.Matrix4CreateFromGLMatrix4(projection), viewport); Vector2f viewportProportion = MatrixMath.V2Make(nearModelPoint.getX(), nearModelPoint.getY()); setZoomPercentage(newPercentage); scrollModelPoint(modelPoint, viewportProportion); // (tickles itself, // public API) }// end setZoomPercentage:preservePoint: // ========== scrollModelPoint:toViewportProportionalPoint: // ===================== // // Purpose: Scroll a given 3-d point on our model to a particular location // on screen. The view location is a ratio of the visible portion // of the screen, e.g. 0.5, 0.5 is the center of the screen. // // ============================================================================== public void scrollModelPoint(Vector3f modelPoint, Vector2f viewportPoint) { if (locationMode == LocationModeT.LocationModeWalkthrough) return; Vector2f newCenter = Vector2f.getZeroVector2f(); float zEval = 0; float zNear = 0; Matrix4 modelViewMatrix = MatrixMath .Matrix4CreateFromGLMatrix4(modelView); Vector4f transformedPoint = Vector4f.getZeroVector4f(); Box2 newVisibleRect = Box2.getZeroBox2(); Box2 currentClippingRect = Box2.getZeroBox2(); Box2 newClippingRect = Box2.getZeroBox2(); // For the camera calculation, we need effective world coordinates, not // model coordinates. transformedPoint = MatrixMath.V4MulPointByMatrix( MatrixMath.V4FromVector3f(modelPoint), modelViewMatrix); // Perspective distortion makes this more complicated. The camera is in // a // fixed position, but the frustum changes with the scrollbars. We need // to // calculate the world point we just clicked on, then derive a new // frustum // projection centered on that point. if (projectionMode == ProjectionModeT.ProjectionModePerspective) { currentClippingRect = nearFrustumClippingRectFromVisibleRect(scroller .getVisibleRect()); // Consider how perspective projection works: you can think of the // frustum as having two // effects on X and Y coordinates: // // (1) it makes them get closer together as they get farther from // the camera. Think of train // tracks converging on the horizon. // // (2) it rescales the entire mess of coordinates from an arbitrary // range of camera X and Y // to -1..1 (after perspective divide) which then go to the // viewport. // // You can think of part 2 as happening via the // left/right/top/bottom inputs to the frustum. // The near plane is used to implement idea 1 - drawing _at_ the // near clip plane goes on to // step 2 unmodified. Anything farther than the near clip plane // becomes smaller. // // (The far clip plane never actually shows up in the final // computation of clip-space x or y.) // // So...transformedPoint is a point in eye space and we want to know // where in our NS document // it is - but our viewport is in model coordinates. IF the point // were on the near clip plane, // this would be no problem; the eye coordinates are what we want. // // So what we do is calculate that 'foreshortening ratio' - that is, // the fraction that makes // the tracks closer to the origin at farther distances. We apply // that to our point, finding // where it 'looks' to the user (farther away is closer to the // camera origin) and we pass that // without ever using step (2) to go from model units to -1..1. // We need the near clip plane - note that it will have a negative // value - since +Z looks at // us EVERYTHING you ever see in GL (in eye coordinates) has // negative Z. zNear = (cameraDistance + fieldDepth() / 2); // The ratio of 'far away' is given by near/z. Both zNear and our // point's Z are negative, so // the ratio is positive, as expected. At the near clip plane the // ratio is 1. Note that IF // we could draw in front of the near clip plane without, y'know, // clipping, then zEval would // become larger than 1 and rapidly head off to infinity as our // transformed point approached // zero. In other words, things heading at us through the near clip // plane would get // infinitely big just as they crash into our eyeballs. zEval = zNear / transformedPoint.getZ(); // New center is eye coordinates of our point scaled in to account // for perspective. newCenter.setX(zEval * transformedPoint.getX()); newCenter.setY(zEval * transformedPoint.getY()); // Calculate a NEW frustum clipping rect centered on the clicked // point's // projection onto the near clipping plane. newClippingRect.setSize(currentClippingRect.getSize()); newClippingRect.getOrigin().setX( newCenter.getX() - MatrixMath.V2BoxWidth(currentClippingRect) * viewportPoint.getX()); newClippingRect.getOrigin().setY( newCenter.getY() - MatrixMath.V2BoxHeight(currentClippingRect) * viewportPoint.getY()); // Reverse-derive the correct Cocoa view visible rect which will // result // in the desired clipping rect to be used. newVisibleRect = visibleRectFromNearFrustumClippingRect(newClippingRect); } else { currentClippingRect = nearOrthoClippingRectFromVisibleRect(scroller .getVisibleRect()); // Ortho centers are trivial. newCenter.setX(transformedPoint.getX()); newCenter.setY(transformedPoint.getY()); // Calculate a clipping rect centered on the clicked point's // projection. newClippingRect.setSize(currentClippingRect.getSize()); newClippingRect.getOrigin().setX( newCenter.getX() - MatrixMath.V2BoxWidth(currentClippingRect) * viewportPoint.getX()); newClippingRect.getOrigin().setY( newCenter.getY() - MatrixMath.V2BoxHeight(currentClippingRect) * viewportPoint.getY()); // Reverse-derive the correct Cocoa view visible rect which will // result // in the desired clipping rect to be used. newVisibleRect = visibleRectFromNearOrthoClippingRect(newClippingRect); } // Scroll to it. -makeProjection will now derive the exact frustum or // ortho // projection which will make the clicked point appear in the center. scroller.setScrollOrigin(newVisibleRect.getOrigin()); tickle(); // Tickle to rebuild all matrices based on external change. }// end scrollModelPoint:toViewportProportionalPoint: // ========== setViewingAngle: // ================================================== // // Purpose: Change the viewing angle to a specific angle. // // ============================================================================== public void setViewingAngle(Vector3f newAngle) { float gl_angle[] = new float[16]; float[] gl_flip = new float[16]; Matrix4 angle = MatrixMath.Matrix4RotateModelview( Matrix4.getIdentityMatrix4(), newAngle); MatrixMath.Matrix4GetGLMatrix4(angle, gl_angle); GLMatrixMath.buildRotationMatrix(gl_flip, 180, 1, 0, 0); GLMatrixMath.multMatrices(orientation, gl_flip, gl_angle); makeModelView(); }// end setViewingAngle: // ========== setProjectionMode: // ================================================ // // Purpose: Change projection modes. // // Notes: This is a special-case - normally we'd tickle, but we only need // to make the projection matrix over because right now all // projection modes keep the same document size. // // ============================================================================== public void setProjectionMode(ProjectionModeT newProjectionMode) { projectionMode = newProjectionMode; makeProjection(); // This doesn't need a full tickle because proj mode // doesn't change the doc size. }// end setProjectionMode: // ========== setLocationMode: // ================================================ // // Purpose: Change Location modes. // // ============================================================================== public void setLocationMode(LocationModeT newLocationMode) { if (locationMode != newLocationMode) { locationMode = newLocationMode; // Tell NS that sizes have changed - once we do this, we can request // a re-scroll. if (locationMode == LocationModeT.LocationModeWalkthrough) scroller.setScaleFactor(1.0f); else scroller.setScaleFactor(zoomFactor / 100.0f); tickle(); } }// end setProjectionMode: // ========== rotationDragged // =================================================== // // Purpose: Rotate the camera based on a 2-d drag vector. // // ============================================================================== public void rotationDragged(Vector2f viewDirection) { float deltaX = viewDirection.getX(); float deltaY = -viewDirection.getY(); // Apple's delta is backwards, for // some reason. // Get the percentage of the window we have swept over. Since half the // window represents 180 degrees of rotation, we will eventually // multiply this percentage by 180 to figure out how much to rotate. float percentDragX = deltaX / scroller.getDocumentSize().getWidth(); float percentDragY = deltaY / scroller.getDocumentSize().getHeight(); // Remember, dragging on y means rotating about x. float rotationAboutY = +(percentDragX * 180); float rotationAboutX = -(percentDragY * 180); // multiply by -1, // as we need to convert our drag into a proper rotation // direction. See notes in function header. if (USE_TURNTABLE) { Vector3f view_now = viewingAngle(); if (view_now.getX() * view_now.getY() * view_now.getZ() < 0.0) rotationAboutY = -rotationAboutY; } // Get the current transformation matrix. By using its inverse, we can // convert projection-coordinates back to the model coordinates they // are displaying. Matrix4 inversed = MatrixMath.Matrix4Invert(MatrixMath .Matrix4CreateFromGLMatrix4(getModelView())); // clear any translation resulting from a rotation center float[][] element = inversed.getElement(); element[3][0] = 0; element[3][1] = 0; element[3][2] = 0; // Now we will convert what appears to be the vertical and horizontal // axes into the actual model vectors they represent. Vector4f vectorX = new Vector4f(1, 0, 0, 1); // unit vector i along // x-axis. Vector4f vectorY = new Vector4f(0, 1, 0, 1); // unit vector j along // y-axis. Vector4f transformedVectorX; Vector4f transformedVectorY; // We do this conversion from screen to model coordinates by multiplying // our screen points by the modelview matrix inverse. That has the // effect of "undoing" the model matrix on the screen point, leaving us // a model point. transformedVectorX = MatrixMath.V4MulPointByMatrix(vectorX, inversed); transformedVectorY = MatrixMath.V4MulPointByMatrix(vectorY, inversed); if (USE_TURNTABLE) { rotationAboutY = -rotationAboutY; transformedVectorY = vectorY; } // Now rotate the model around the visual "up" and "down" directions. GLMatrixMath.applyRotationMatrix(orientation, rotationAboutX, transformedVectorX.getX(), transformedVectorX.getY(), transformedVectorX.getZ()); GLMatrixMath.applyRotationMatrix(orientation, rotationAboutY, transformedVectorY.getX(), transformedVectorY.getY(), transformedVectorY.getZ()); makeModelView(); }// end rotationDragged // ========== rotateByDegrees: // ================================================== // // Purpose: Rotate the camera by a fixed angle - used by the trackpad twist // gesture, this rotates aronud the screen Y axis. // // ============================================================================== public void rotateByDegrees(float angle) { GLMatrixMath.applyRotationMatrix(orientation, angle, 0, -1, 0); makeModelView(); }// end rotateByDegrees: }